Skip to content

fix: shut down stdio server on stdin EOF/close#44

Open
guhans6 wants to merge 1 commit into
kimsungwhee:mainfrom
guhans6:fix/stdin-shutdown-cleanup
Open

fix: shut down stdio server on stdin EOF/close#44
guhans6 wants to merge 1 commit into
kimsungwhee:mainfrom
guhans6:fix/stdin-shutdown-cleanup

Conversation

@guhans6
Copy link
Copy Markdown

@guhans6 guhans6 commented May 5, 2026

Summary

  • add a single guarded shutdown path to prevent duplicate exits
  • exit when stdio parent disconnects by handling process.stdin end and close
  • hook StdioServerTransport onclose and reuse the same shutdown path for SIGINT/SIGTERM/unhandled errors

Why

In stdio MCP hosts, parent/client disconnect can close stdin without delivering a signal. Without stdin close handling, apple-docs-mcp can remain orphaned and accumulate over time.

Verification

  • baseline (main): process remains alive after stdin close
  • patched branch: process exits within about 0.2s after stdin close

Summary by CodeRabbit

  • Bug Fixes

    • Fixed issue where application shutdown could be triggered multiple times
  • Refactor

    • Unified shutdown and error handling flow for improved consistency and reliability
    • Updated executable behavior to require explicit initialization

Prevent orphaned MCP processes by handling stdin EOF/close and transport onclose with a guarded shutdown path.
@guhans6 guhans6 mentioned this pull request May 5, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

📝 Walkthrough

Walkthrough

The PR unifies process shutdown by introducing a guarded shutdown() helper method and rerouting error handlers (SIGINT, SIGTERM, unhandled rejections, exceptions) and transport close events through it instead of calling process.exit() directly. The executable entrypoint block is removed.

Changes

Unified Shutdown Flow

Layer / File(s) Summary
State & Core Mechanism
src/index.ts (lines 30–42)
isShuttingDown flag and shutdown(exitCode, reason?) method ensure single, logged shutdown path.
Error Handler Wiring
src/index.ts (lines 282–309)
SIGINT, SIGTERM, stdin end/close, unhandledRejection, and uncaughtException now route through shutdown() instead of direct process.exit().
Transport Integration
src/index.ts (lines 311–316)
Transport close event calls shutdown(0, 'transport close') for consistency.
Cleanup
src/index.ts (line 344)
Removes top-level executable block that auto-instantiated and ran the server outside test mode.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes


🐰 A scattered shutdown now tamed,
One method keeps the promise framed,
No more exits rogue and wild—
The rabbit's closure, calm and mild.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix: shut down stdio server on stdin EOF/close' directly and clearly summarizes the main change: adding shutdown handling for stdin EOF/close events. It is concise, specific, and accurately reflects the primary objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/index.ts (1)

340-343: ⚡ Quick win

Keep startup-failure termination on the same shutdown path.

At Line 340-343, process.exit(1) still bypasses the new guarded shutdown path (reason logging + single-exit semantics). Consider exposing a small public stop()/shutdown() entrypoint and using it here for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 340 - 343, Replace the direct process.exit(1) in
the server.run() catch handler with a call to a single guarded shutdown
entrypoint (e.g., export an async stop() or shutdown(reason?: Error|number) on
your app/server module) so startup failures follow the same shutdown path;
update the catch to call shutdown(error) after logger.error('Fatal error in
main():', error) and ensure the new shutdown/stop implementation performs the
reason logging, runs cleanup, and enforces single-exit semantics (only then call
process.exit with the appropriate code).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/index.ts`:
- Around line 284-298: Replace the persistent listeners with one-shot handlers:
change the process and stdin event registrations that currently call
process.on(...) for 'SIGINT', 'SIGTERM', and the 'end'/'close' listeners on
process.stdin to use .once(...) so each event invokes the class's shutdown(...)
handler only once per process lifecycle; update the registrations around the
shutdown(...) references (the SIGINT, SIGTERM, process.stdin 'end' and 'close'
handlers) accordingly to prevent listener accumulation during repeated test
instantiation.

---

Nitpick comments:
In `@src/index.ts`:
- Around line 340-343: Replace the direct process.exit(1) in the server.run()
catch handler with a call to a single guarded shutdown entrypoint (e.g., export
an async stop() or shutdown(reason?: Error|number) on your app/server module) so
startup failures follow the same shutdown path; update the catch to call
shutdown(error) after logger.error('Fatal error in main():', error) and ensure
the new shutdown/stop implementation performs the reason logging, runs cleanup,
and enforces single-exit semantics (only then call process.exit with the
appropriate code).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8faed29b-dc44-4938-a3ef-743e23e0ffe5

📥 Commits

Reviewing files that changed from the base of the PR and between 28c06cb and 168ab87.

📒 Files selected for processing (1)
  • src/index.ts

Comment thread src/index.ts
Comment on lines 284 to 298
process.on('SIGINT', () => {
process.exit(0);
this.shutdown(0, 'SIGINT');
});

process.on('SIGTERM', () => {
process.exit(0);
this.shutdown(0, 'SIGTERM');
});

process.stdin.on('end', () => {
this.shutdown(0, 'stdin end');
});

process.stdin.on('close', () => {
this.shutdown(0, 'stdin close');
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's read the specific lines in question
head -n 310 src/index.ts | tail -n +280 | cat -n

Repository: kimsungwhee/apple-docs-mcp

Length of output: 996


🏁 Script executed:

# Get more context around the shutdown setup
head -n 350 src/index.ts | tail -n +270 | cat -n

Repository: kimsungwhee/apple-docs-mcp

Length of output: 2740


🏁 Script executed:

# Check the class structure and constructor
head -n 100 src/index.ts | cat -n

Repository: kimsungwhee/apple-docs-mcp

Length of output: 4313


🏁 Script executed:

# Check if this class is instantiated multiple times in tests or elsewhere
rg -n "new\s+\w+Server\|new\s+\w+App\|new\s+\w+Manager" --type ts --type tsx

Repository: kimsungwhee/apple-docs-mcp

Length of output: 98


🏁 Script executed:

# Look for any test files that might instantiate this class multiple times
fd -e test.ts -e test.tsx -e spec.ts -e spec.tsx | head -20

Repository: kimsungwhee/apple-docs-mcp

Length of output: 845


🏁 Script executed:

# Check the export and how the class is used
rg -n "export.*class|class.*{" src/index.ts -A 3

Repository: kimsungwhee/apple-docs-mcp

Length of output: 178


🏁 Script executed:

# Check basic test file
head -n 100 tests/basic.test.ts | cat -n

Repository: kimsungwhee/apple-docs-mcp

Length of output: 1109


🏁 Script executed:

# Check index test file
head -n 100 tests/index.test.ts | cat -n

Repository: kimsungwhee/apple-docs-mcp

Length of output: 3701


🏁 Script executed:

# Search for any test setup that creates multiple instances
rg -n "new AppleDeveloperDocsMCPServer" --type ts

Repository: kimsungwhee/apple-docs-mcp

Length of output: 913


🏁 Script executed:

# Check if there are any test fixtures or utilities that instantiate the server
rg -n "AppleDeveloperDocsMCPServer" --type ts -B 2 -A 2

Repository: kimsungwhee/apple-docs-mcp

Length of output: 5333


Use one-shot handlers for process/stdin shutdown events to prevent listener accumulation in tests.

The listeners at lines 284-298 are registered with .on(...) in the constructor. Since this class is instantiated multiple times in test suites (beforeEach patterns in tests/index.test.ts, tests/response-format.test.ts, and regression tests), handlers accumulate on the global process and process.stdin objects even though isShuttingDown prevents duplicate exits. Switching to .once(...) is appropriate since these signals and stdin events should only trigger shutdown once per process lifecycle.

Suggested patch
-    process.on('SIGINT', () => {
+    process.once('SIGINT', () => {
       this.shutdown(0, 'SIGINT');
     });

-    process.on('SIGTERM', () => {
+    process.once('SIGTERM', () => {
       this.shutdown(0, 'SIGTERM');
     });

-    process.stdin.on('end', () => {
+    process.stdin.once('end', () => {
       this.shutdown(0, 'stdin end');
     });

-    process.stdin.on('close', () => {
+    process.stdin.once('close', () => {
       this.shutdown(0, 'stdin close');
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
process.on('SIGINT', () => {
process.exit(0);
this.shutdown(0, 'SIGINT');
});
process.on('SIGTERM', () => {
process.exit(0);
this.shutdown(0, 'SIGTERM');
});
process.stdin.on('end', () => {
this.shutdown(0, 'stdin end');
});
process.stdin.on('close', () => {
this.shutdown(0, 'stdin close');
});
process.once('SIGINT', () => {
this.shutdown(0, 'SIGINT');
});
process.once('SIGTERM', () => {
this.shutdown(0, 'SIGTERM');
});
process.stdin.once('end', () => {
this.shutdown(0, 'stdin end');
});
process.stdin.once('close', () => {
this.shutdown(0, 'stdin close');
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 284 - 298, Replace the persistent listeners with
one-shot handlers: change the process and stdin event registrations that
currently call process.on(...) for 'SIGINT', 'SIGTERM', and the 'end'/'close'
listeners on process.stdin to use .once(...) so each event invokes the class's
shutdown(...) handler only once per process lifecycle; update the registrations
around the shutdown(...) references (the SIGINT, SIGTERM, process.stdin 'end'
and 'close' handlers) accordingly to prevent listener accumulation during
repeated test instantiation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant